Skip to content

Multi-body bookings: divisional and multi-body visibility scopes (prod) - #22

Merged
pataniaeli merged 8 commits into
mainfrom
dev
Aug 26, 2026
Merged

Multi-body bookings: divisional and multi-body visibility scopes (prod)#22
pataniaeli merged 8 commits into
mainfrom
dev

Conversation

@pataniaeli

Copy link
Copy Markdown
Collaborator

See PR to dev

pataniaeli and others added 8 commits August 25, 2026 22:17
Bookings and room_requests gain a `scope` of single | divisional | multi.
`body_id` stays populated in every case as the originating body, so all 48
existing bookings and 13 requests remain valid with no backfill, and audit
log / email attribution never depends on the scope.

- schema: scope + division columns, booking_bodies and room_request_bodies
  join tables, CHECK constraints, partial indexes on the divisional path.
- rls: generalizes the policies that previously resolved visibility through
  a single body_id. booking_is_visible / booking_is_manageable are now the
  single definition, delegated to by every child-table policy so the rule
  cannot drift. Also pins search_path on the three pre-existing helpers,
  clearing a live function_search_path_mutable finding.
- grants: new helpers are internal RLS predicates, not API surface, so
  EXECUTE is revoked from PUBLIC/anon. `authenticated` must keep it --
  verified that policy evaluation itself requires EXECUTE on functions the
  policy calls.

Includes a rollback script captured verbatim from pg_policies before the
rewrite, so the prior behavior can be restored exactly rather than from
memory.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adds lib/booking-scope.ts as the single app-side home for the scope rule,
mirroring booking_is_visible / booking_is_manageable in SQL.

RLS only guards reads here -- every booking and request write goes through
the service-role client, which bypasses RLS entirely -- so the guards in
this module are the real write authorization. Shipping the migration
without these would let leadership request a divisional booking for a
division they do not lead.

- replaces the single-body Leadership check duplicated across the revision,
  cancellation and request routes with requireBookingManager.
- the three admin booking routes persist scope and sync booking_bodies on
  both create and edit, clearing the join rows when the scope moves away
  from multi.
- resolveBookingRecipients replaces six per-route membership queries that
  only ever notified the owning body, which would under-notify divisional
  and multi bookings. Multi notifies all listed bodies; divisional notifies
  the owning body plus peer leadership, so a whole division is not emailed
  on every edit.
- my-rooms can no longer express visibility as one .in(body_id): PostgREST
  has no subquery syntax, so the divisional and multi paths are resolved to
  ids first and folded into a single .or(). canManage is now computed
  server-side per booking instead of derived client-side from a flat
  leadership list, which only ever worked for single-body bookings.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
BookingScopeSelector replaces the plain Body <select> that the three admin
create forms, three admin edit forms, and the leadership request page each
declared inline. Single body stays the default and still reads as one
field, so the common case is unchanged.

Admins may scope to any division; leadership only to divisions they lead,
and the option is disabled with a hint when they lead none. The multi-body
pool is every active body for everyone, since any leadership may request a
multi-body booking with any combination -- that leaks nothing, as bodies
are already readable by any authenticated user. Switching scope clears the
fields the new scope does not use, so the payload always satisfies the
database CHECK constraints.

ScopeLabel renders "Campus Affairs (Division)" and "DEI Committee + 2
others" with the full list on hover and click-to-expand. My Rooms now
groups by scope rather than by body, since for divisional and multi
bookings the owning body is not what determines who sees them.

public/sw.js is the regenerated next-pwa artifact.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`supabase db dump` writes data.sql / roles.sql / schema.sql into the repo
root. These should never be committed: a populated roles.sql can contain
role passwords and data.sql is a full copy of production. Schema history
belongs in supabase/migrations/.

The three currently present are 0 bytes, from an interrupted dump.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Resolves two conflicts in My Rooms, where the #14/#13/#16 rework landed on
the same files as the scope changes.

- app/api/my-rooms/route.ts: kept the scope-aware visibility rewrite (the
  three-way OR across body / division / booking_bodies, and server-computed
  canManage) and folded in dev'"'"'s senate_type_preferences fetch and return.
  The old leadershipBodyIds response field stays dropped -- it only ever
  described single-body bookings.
- app/(dashboard)/my-rooms/page.tsx: took dev'"'"'s reworked page as the base and
  reapplied the scope changes onto it, rather than the reverse, so the new
  list/calendar toggle, search, status filter and Senate preferences are all
  preserved.

FlatBooking now lives in dev'"'"'s shared.ts, so canManage / scopeKey / scopeLabel
and the scopeKeyOf / scopeLabelOf helpers moved there too -- calendar-view and
booking-detail-modal consume the same type and now show the scope label rather
than the owning body, matching the rest of My Rooms.

Their #15 bcc fix composes well with the wider divisional/multi fan-out: the
larger recipient list is no longer exposed via to:/cc:.

tsc clean, build succeeds, and the only lint findings in the touched area are
pre-existing ones in notification-bell.tsx and LoginCard.tsx.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
booking_bodies and room_request_bodies were created with a composite
(parent_id, body_id) primary key -- the textbook junction shape. PostgREST
detects that and began inferring a many-to-many `bookings <-> bodies`
relationship through booking_bodies, on top of the existing many-to-one
bookings.body_id -> bodies.id.

With two candidate relationships every `bodies(name)` embed became ambiguous
and PostgREST answered 300 / PGRST201. That broke reads app-wide, including
code that predates this feature -- creating the tables was enough on its own.
The Administrator and My Rooms pages showed "no bookings found" while all 48
bookings were still present.

board_memberships is the precedent already in this schema: also a junction
table, but with a surrogate id primary key and the pair merely UNIQUE, which
is why users <-> bodies has never been ambiguous. The junction tables now
match it. The unique constraint preserves the real invariant; only the
backing index changes.

Also stops the two affected read routes from coercing a failed query into an
empty list. `data || []` is what turned a hard API error into a calm "no
bookings found" -- the failure was indistinguishable from having no data,
which is why this looked like missing rows rather than a broken query.

Verified over the REST API that both the pre-existing `bodies(name)` embeds
and the new nested ones return 200, and re-ran the RLS persona matrix to
confirm the primary key change did not affect visibility.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A hidden booking is only visible to those who can manage it, but
resolveBookingRecipients never consulted `hidden` -- so every member of the
audience got a user_alert and an email whenever one was updated, telling them
about a booking they cannot see.

Hidden now narrows recipients to Leadership across the whole audience, for all
three scopes, which is exactly the set canManageScoped() admits. Confirmed
against real data: a hidden Campus Affairs divisional booking previously
notified 7 people including 1 plain member, and now notifies the 6 leadership.

`hidden` is looked up inside resolveBookingRecipients rather than passed in by
each route, because a caller forgetting to pass it is precisely what caused
this bug. One lookup, no route can bypass it.

Visibility itself was already correct and needed no change: my-rooms filters on
`!hidden || canManage`, and canManage is scope-aware, so members are excluded
from hidden single-body, divisional and multi bookings alike.

Bumps the version to 1.13.0 and moves the FAQ roadmap heading to v1.14.0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Multi-body bookings: divisional and multi-body visibility scopes (#19)
@pataniaeli pataniaeli self-assigned this Aug 26, 2026
@vercel

vercel Bot commented Aug 26, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
chambers Ready Ready Preview Aug 26, 2026 3:04am

@pataniaeli
pataniaeli merged commit a5e9d6a into main Aug 26, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant